﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Muel.TypeScriptFacade.Core
{
    public static class TypeHelper
    {

        public static IEnumerable<Type> GetSubTypes(this Type type)
        {
            return type.Assembly.GetTypes()
                .Where(t => 
                    t.BaseType!= null &&
                    !t.BaseType.IsUnderlyingPrimitive() && 
                    t.BaseType.Name.Equals(type.Name));
        }

        public static bool HasAttributeOfType<TAttribute>(this MemberInfo type, bool inherit)
        {
            object[] attributes = type.GetCustomAttributes(inherit);
            string memberName = type.Name;
            bool isEmpty = string.IsNullOrEmpty(memberName);
            if (isEmpty)
            {
                memberName = "blah";
            }

            // if we're inheriting, search interfaces
            if (inherit)
            {
                if (attributes == null)
                {
                    attributes = new object[0];
                }

                if (type is Type && !((Type)type).IsInterface)
                {
                    IEnumerable<object> interfaceAttributes = ((Type)type).GetInterfaces().SelectMany(t => t.GetCustomAttributes(inherit));
                    attributes = attributes.Concat(interfaceAttributes).ToArray();
                }
                else if (type is MethodInfo && !((MethodInfo)type).DeclaringType.IsInterface)
                {
                    MethodInfo method = (MethodInfo)type;
                    MethodInfo[] interfaceMethods = method.GetInterfaceMethods();
                    IEnumerable<object> interfaceAttributes = interfaceMethods.SelectMany(m => m.GetCustomAttributes(inherit));
                    attributes = attributes
                        .Concat(interfaceAttributes)
                        .ToArray();
                }
            }

            if (attributes == null || attributes.Length == 0)
            {
                return false;
            }
            return attributes.OfType<TAttribute>().Any();
        }

        public static bool HasAttributeOfType<TAttribute>(this MemberInfo type)
        {
            return type.HasAttributeOfType<TAttribute>(false);
        }

        public static MethodInfo[] GetInterfaceMethods(this MethodInfo method)
        {
            Type declaringType = method.DeclaringType;
            Type[] implementedInterfaces = declaringType.GetInterfaces();
            Type[] methodParameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
            if (implementedInterfaces != null && implementedInterfaces.Any())
            {
                return implementedInterfaces
                    .Select(type => type.GetMethod(method.Name, methodParameterTypes))
                    .Where(type => type != null)
                    .ToArray();
            }
            return new MethodInfo[0];
        }


        /// <summary>
        /// Returns true if the type (or if it's an array the element type) is primitive/string/void
        /// </summary>
        /// <param name="type">The type</param>
        /// <returns>True if primitive/string/void, false otherwise</returns>
        public static bool IsUnderlyingPrimitive(this Type type)
        {
            bool isCollection = type.IsCollection();
            bool isDictionary = type.IsDictionary();

            bool returnValue = 
                (!isDictionary && !isCollection && type.Module.ScopeName.Equals("CommonLanguageRuntimeLibrary"))
                ||
                isDictionary 
                ||
                (isCollection && type.GetCollectionTypeArgument().IsUnderlyingPrimitive());
            return returnValue;
        }

        public static Type GetCollectionTypeArgument(this Type type)
        {
            if (type.IsArray)
            {
                return type.GetElementType();
            }
            else if (type.IsCollection())
            {
                Type[] argumentTypes = type.GetGenericArguments();
                return argumentTypes.Single();
            }
            else
            {
                return type.GetElementType();
            }
        }

        public static bool IsGenericOfType(this Type type, Type genericType)
        {
            return (type.IsGenericType && type.GetGenericTypeDefinition() == genericType);
        }

        public static bool IsDictionary(this Type type)
        {
            return type.IsGenericOfType(typeof(Dictionary<,>)) || type.IsEquivalentTo(typeof(System.Dynamic.ExpandoObject));
        }

        public static bool IsCollection(this Type type)
        {
            return type.IsArray || type.IsGenericOfType(typeof(List<>));
        }
        
        /// <summary>
        /// Determines if a type is numeric.  Nullable numeric types are considered numeric.
        /// </summary>
        /// <remarks>
        /// Boolean is not considered numeric
        /// </remarks>
        public static bool IsNumeric(this Type type)
        {
            if (type == null)
            {
                return false;
            }

            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Byte:
                case TypeCode.Decimal:
                case TypeCode.Double:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.SByte:
                case TypeCode.Single:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    return true;
                case TypeCode.Object:
                    if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                    {
                        return Nullable.GetUnderlyingType(type).IsNumeric();
                    }
                    return false;
            }
            return false;
        }
    }
}
